﻿
package extremefx.collections {
	import extremefx.IEnumerable;
	import extremefx.IEnumerator;
	import extremefx.conditions.requires;
	
	import flash.net.registerClassAlias;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;		

	/**
	 * @author Marcelo Volmaro
	 */
	 
	public final class TreeNode implements ICollection, IEnumerable {
		registerClassAlias("extremefx.collections.TreeNode", TreeNode);
		
		private var _value:*;
		internal var _first:TreeNode;
		internal var _last:TreeNode;
		private var _children:uint;
		
		internal var _parent:TreeNode;
		internal var _next:TreeNode;
		internal var _prev:TreeNode;

		public function TreeNode(pValue:* = null, pParent:TreeNode = null) {
			_value = pValue;
			_children = 0;
			
			if (pParent){
				_parent = pParent;
				pParent.appendChild(this);
			}
		}
		
		public function get value():* {
			return _value;
		}

		/**
		 * returns an array of child nodes of the given element.
		 */
		public function get children():Array {
			var a:Array = [];
			var node:TreeNode = _first;

			while (node) {
				a.push(node._value);
				node = node._next;
			}
			
			return a;
		}
		
		/**
		 * Returns the node's first child in the tree, or null if the node is childless.
		 */
		public function get firstChild():TreeNode {
			return _first;
		}
		
		/**
		 * Returns the node's last child in the tree, or null if the node is childless.
		 */
		public function get lastChild():TreeNode {
			return _last;
		}
		
		/**
		 * Returns the node immediately following the given one in the tree, or null if there is no sibling node.
		 */
		public function get nextSibling():TreeNode {
			return _next;
		}
		
		/**
		 * Returns the node immediately preceding the given one in the tree, or null if there is no sibling node.
		 */
		public function get previousSibling():TreeNode {
			return _prev;
		}
		
		/**
		 * The parent element of this node, or null if the node is a root node or a node not attached to any tree.
		 */
		public function get parent():TreeNode {
			return _parent;
		}
		
		public function getDepth():uint {
			var l:uint = 0, n:TreeNode = _parent;
			while (n) {
				++l;
				n = n._parent;
			}
			
			return l;
		}
		
		public function getRoot():TreeNode {
			var n:TreeNode = _parent, o:TreeNode = this;
			
			while (n) {
				o = n;
				n = n._parent;
			}
			
			return o;
		}
		
		public function isRoot():Boolean {
			return _parent == null;
		}
		
		public function hasChildren():Boolean {
			return (_first !== _last);
		}
		
		public function hasSiblings():Boolean {
			if (_parent) return (_parent._first !== this || _parent._last !== this);
			return false;
		}
		
		public function isEmpty():Boolean {
			return _first == null;
		}
		
		public function appendChild(pNode:TreeNode):TreeNode {
			pNode.remove();
			
			var l:TreeNode = _last;
			
			if (l) {//Insert at the end
				pNode._prev = l;
				_last = l._next = pNode;
				
			} else {//First child element
				_first = _last = pNode;
			}
			
			pNode._parent = this;
			_children++;
			
			return pNode;
		}
		
		/**
		 * Inserts the specified node before a reference element as a child of the current node.
		 */
		public function insertBefore(pAdjacentNode:TreeNode, pInsertNode:TreeNode):TreeNode {
			if (pAdjacentNode){
				pInsertNode.remove();
				
				if (pAdjacentNode == _first) _first = pInsertNode;
				
				pInsertNode._prev = pAdjacentNode._prev;
				pInsertNode._next = pAdjacentNode;
				pAdjacentNode._prev = pInsertNode;
				
				pInsertNode._parent = this;
				_children++;
				
			} else {
				appendChild(pInsertNode);
			}
			
			return pInsertNode;
		}
		
		/**
		 * Inserts the specified node after a reference element as a child of the current node.
		 */
		public function insertAfter(pAdjacentNode:TreeNode, pInsertNode:TreeNode):TreeNode {
			if (pAdjacentNode && pAdjacentNode._next) {
				return insertBefore(pAdjacentNode._next, pInsertNode);
				
			} else {
				return appendChild(pInsertNode);
			}
		}
		
		public function remove():void {
			var p:TreeNode = _parent;
			if (!p) return;
			
			var nx:TreeNode = _next;
			var pr:TreeNode = _prev;
			
			if (p._first == this) {
				p._first = nx;
				nx._prev = null;
				
			} else if (p._last == this){
				p._last = pr;
				pr._next = null;
				
			} else {
				pr._next = nx;
				nx._prev = pr;
			}
			
			p._children--;
			_prev = _next = _parent = null;
		}
		
		/**
		 * Removes a child node from the current element.
		 * If child is actually not a child of the element node, the method throws an exception.
		 */
		public function removeChild(pNode:TreeNode):TreeNode{
			requires(pNode._parent, "node").isNotSameAs(this);
			
			pNode.remove();
			return pNode;
		}
		
		/**
		 * Replaces one child node of the specified element with another.
		 * 
		 */
		public function replaceChild(pNewNode:TreeNode, pOldNode:TreeNode):TreeNode{
			requires(pOldNode._parent, "node").isNotSameAs(this);
			
			if (pOldNode == _first){
				insertAfter(pOldNode, pNewNode);
				
			} else {
				insertBefore(pOldNode, pNewNode);
			}
			
			pOldNode.remove();
			return pOldNode;
		}
		
		public function toArray():Array {
			var e:IEnumerator = getEnumerator();
			var a:Array = new Array();
			
			while(e.moveNext()) {
				a.push(e.current.value);				
			}
			
			return a;
		}
		
		public function getEnumerator():IEnumerator {
			return new TPreEnum(this);
		}
		
		public function getPreOrderEnumerator():IEnumerator {
			return new TPreEnum(this);
		}
		
		public function getPostOrderEnumerator():IEnumerator {
			return new TPostEnum(this);
		}
		
		public function readExternal(input:IDataInput):void {
			_value = input.readObject();
			_first = input.readObject();
			_last = input.readObject();
			_parent = input.readObject();
			_next = input.readObject();
			_prev = input.readObject();
			_children = input.readUnsignedInt();
		}
		
		public function writeExternal(output:IDataOutput):void {
			output.writeObject(_value);
			output.writeObject(_first);
			output.writeObject(_last);
			output.writeObject(_parent);
			output.writeObject(_next);
			output.writeObject(_prev);
			output.writeUnsignedInt(_children);
		}
		
		public function get count():uint {
			var count:uint = 1;
			var node:TreeNode = _first;
			while (node) {
				count += node.count;
				node = node._next;
			}
			
			return count;
		}
	}
}